<?php
namespace DRY\Module;

/**
 * DonReY Framework 2012 :: Layout module - "View" manager in an MVC pattern - Pull mode
 *
 * This is an inverted template system, in which the template (layout) controlls what code is executed and what data is retrieved.
 * Applications can contain "widgets" or use predefined ones, and the layout calls for execution of a widget using placehoders formatted like:
 *   <?widget:some data?> or ~?widget:some data?~
 * A layout usually represents the HTML code that is used to generate a web page, but it can be something different ( that's why the two formats for placeholders )
 * "widget" is a name in the format "name" or "category/name"... "category/subcategory/name" etc.
 *   Categories are actually directories in the widget repository, so they need to have valid filesystem names.
 * "some data" can be anything (except ending characters "?>" or "?~"), this data is passed on to the widget.
 *   The data can be a reference to a config value or some variable that was prepared before running the layout system or just simple "data" ( a string ).
 *   Only the widget knows what to do with it, there are no fixed rules.
 * The string returned by a widget is then used to replace the placeholder from the layout.
 * All text outside placeholders is returned unmodified.
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/layout.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module\Layout
 */
class Layout extends base
{
	// Exception/Error codes
	const
		EXC_PATHS_NOTSET = 1,
		EXC_PATHS_MISSING = 2,
		EXC_WIDGET_NOTALLOWED = 4,
		EXC_WIDGET_ID_NOTFOUND = 5,
		EXC_WIDGET_NOTFOUND = 6,
		EXC_WIDGET_INSIDE_THROW = 9
		;

	/**
	 * Handler for commands from the Control module
	 * @param string $command
	 */
	public function control_Command($command)
	{
		switch($command) {
			case 'ExportConfig':
				$exp = array('module'=> array('alias'=> $this->_alias));
				$exp['paths'] = \DRY\core::assemble_Absolute_Paths($this-> CFG-> paths); // APP_PATH is defaut

//				dump($exp);
				return $exp;
		}
	}

	/**
	 * Paths where layouts and widgets are stored (could be in app folders or anywhere else)
	 * @var array[array[string]]
	 */
	protected $_paths = array('layouts'=>array(), 'widgets'=>array());

	/**
	 * Configuration for widgets, automatically sent to widget objects when it exists. Array where:
	 * - key = widget alias
	 * - value = widget config
	 * @var array
	 */
	protected $_widget_CFG;

	/**
	 * Loaded widgets as an array
	 * - key: widget_identifier
	 * - value: widget_object
	 * @var array[DRY_Widget]
	 */
	private $_widgets;

	/**
	 * Object that contains methods that generate data for widgets (can be anything, as long as it has perpare_{widgetname/alias} methods)
	 *
	 * @var stdClass
	 */
	private $_prepare_object;

	/**
	 * Module Constructor
	 * @param string $alias
	 * @param \SimpleXMLElement $CFG
	 */
	public function __construct($alias, $CFG)
	{
		parent::__construct($alias, $CFG);
		if(isset($this-> CFG-> paths)) {
			$this->_paths = \DRY\core::assemble_Absolute_Paths($this-> CFG-> paths);

			// check if there is at least one valid layout path and one valid widget path
			$found = false;
			if(isset($this->_paths['layouts']))
			foreach($this->_paths['layouts'] as $path)
				if($path!==false) {
					$found = true;
					break;
				}
			if(!$found)
				throw new \DRY\Water('layout', self::EXC_PATHS_MISSING, array('what'=>"layouts"));

			$found = false;
			if(isset($this->_paths['widgets']))
			foreach($this->_paths['widgets'] as $path)
				if($path!==false) {
					$found = true;
					break;
				}
			if(!$found)
				throw new \DRY\Water('layout', self::EXC_PATHS_MISSING, array('what'=>"widgets"));
		} else
			throw new \DRY\Water('layout', self::EXC_PATHS_NOTSET);

		// setup autoloading for widget classes
		// Note: Due to the fact that widgets are organized in subdirectories, the default core autoloader is insufficient, so a special one is designed here
		spl_autoload_register(array($this,'__autoload'));

		// Load own widget configuration (if it exists)
		if(isset($this-> CFG-> widgets)) {
			$this-> load_Widget_Config($this-> CFG-> widgets);
		}
	}

	/**
	 * Allows installing of widgets defined somewhere else (not inside this module)
	 * @param \SimpleXMLElement $widget_CFG
	 * @param mixed $extra Extra information inserted into each of the widgets' config
	 */
	public function load_Widget_Config($widgets_Config, $extra = null)
	{
		// check/prepare configuration for widget list, preload those marked so
		foreach($widgets_Config-> children() as $widget_selector=> $widget_CFG) {
			$widget_identifier = isset($widget_CFG['alias'])?(string)$widget_CFG['alias']:$widget_selector;
			$this->_widget_CFG[$widget_identifier] = $widget_CFG;

			// insert extra information in widget config
			if(!is_null($extra))
				if($extra instanceof \SimpleXMLElement)
					$this->_widget_CFG[$widget_identifier]-> add_XML_Child($extra);
				elseif(is_array($extra))
					$this->_widget_CFG[$widget_identifier]-> set_Attributes($extra);
				elseif(is_string($extra))
					$this->_widget_CFG[$widget_identifier]-> addChild('extra', $extra);

			// preload widget
			if($widget_CFG['load'] == 'true') {
				$widget = $this-> widget_Load($widget_identifier);

				// do process for the preloaded widget
				if($widget_CFG['process'] == 'true') {
					if(method_exists($widget, 'process')) // process method is not mandatory
						$widget-> process();
				}
			}
		}
	}

	/**
	 * Class Autoloader for widgets
	 * @param string $class_name
	 */
	public function __autoload($class_name)
	{
		if(substr($class_name,0,18)=='DRY\Layout\Widget\\') {
			$widget_name = strtolower(substr($class_name, 18));

			// if the configuration specifies a list of widgets (which it should), checks if the one required to load is there, if not, deny loading
			if(isset($this-> CFG-> widgets)) {
				$widget_selector = str_replace('_', '.', $widget_name);
				if(!isset($this-> CFG ->widgets ->{$widget_selector}))
					throw new \DRY\Water('layout', self::EXC_WIDGET_NOTALLOWED, array('widget_name'=> $widget_selector));
			}

			// attempt to find the widget in one of the widget paths
			$found = false;
			foreach($this->_paths['widgets'] as $widgets_path)
				if($widgets_path!==false) {
					$path = $widgets_path.'/'.str_replace('_','/',strtolower($widget_name)).'.widget.php';
					if(file_exists($path)) {
						include_once $path;
						\DRY\Water::dump($widget_name, "Layout::Widget");
						$found = true;
						break;
					}
				}
			if(!$found)
				throw new \DRY\Water('layout', self::EXC_WIDGET_NOTFOUND, array('widget_name'=> $widget_selector));
		}
	}

	/**
	 * Loads a widget using it's identifier (selector or alias), saves it in the internal widget repository and returns the widget object (or use the existing one if it exists)
	 *
	 * @param string $widget_identifier
	 */
	public function &widget_Load($widget_identifier)
	{
		if(!isset($this->_widget_CFG[$widget_identifier]))
			throw new \DRY\Water('layout', self::EXC_WIDGET_ID_NOTFOUND, array('widget_identifier'=> $widget_identifier));

		$widget_selector = $this->_widget_CFG[$widget_identifier]-> getName();

		if(!isset($this->_widgets[$widget_identifier])) {
			$widget_class_name = 'DRY\Layout\Widget\\'.str_replace(' ','_',ucwords(str_replace('.',' ',$widget_selector)));
			$this->_widgets[$widget_identifier] = new $widget_class_name($this->_widget_CFG[$widget_identifier], $this); // Send widget configuration to constructor
		}
		return $this->_widgets[$widget_identifier];
	}

	/**
	 * Loads(if not already loaded), initializes and returns output from a widget object
	 *
	 * Format for $placeholder array (coming from the preg_replace_callback():
	 * 0: The complete placeholder <?....?> - unused
	 * 1: widget identifier (selector or alias, reference into $this->_widgets)
	 * 2: prepare method name - when present, a method with that name is called in the prepare object; when not, the method prepare_{Identifier} is called
	 * 3: passed parameter coming from layout file - a random string, any characters except ?> (which ends the placeholder)
	 *
	 * @param array $placeholder "matches" data from preg_replace_callback
	 */
	private function widget_Execute($placeholder)
	{
		try {
			$widget = $this-> widget_Load($placeholder[1]); // [1] = widget identifier
			$prepare_method_name = $placeholder[2]?:'prepare_'.str_replace(' ', '_', ucwords(str_replace('.', ' ', $placeholder[1]))); // [2] = custom method name
			return $widget-> render(
				// if the prepare object exists and contains a valid preparation method
				(!is_null($this->_prepare_object) && method_exists($this->_prepare_object, $prepare_method_name))?
					// call the preparation method, with the string from the layout and the widget object itself as parameters, return of that method is sent to the widget's render() method
					call_user_func(array($this->_prepare_object, $prepare_method_name), $placeholder[3], $widget):
					// or, if not, send directly the data from the template (which might be empty as well)
					$placeholder[3], // or, if not, send directly the data from the template (which might be empty as well)
				$this // sends a reference to itself to widgets ( so the widget can communicate back something after rendering )
			);
		} catch(\Exception $e) {
			// layout or widget bug, in any case, empty string is returned (script continues)
			\DRY\water::caught_Exception($e, 'layout', self::EXC_WIDGET_INSIDE_THROW, array('widget_identifier'=> $placeholder[1]));
			return '';
		}
	}

	/**
	 * Takes a layout file, discovers all widget placeholders in it and returns the layout with replaced data
	 * @param string $layout_file
	 * @param stdClass $prepare_object
	 */
	public function render($layout_file, $prepare_object=null)
	{
		// attempt to find the layout in one of the layout paths:
		$found = false;
		foreach($this->_paths['layouts'] as $layouts_path)
			if($layouts_path!==false) {
				$path = $layouts_path.'/'.$layout_file;
				if(file_exists($path)) {
					$layout = @file_get_contents($path);
					$found = true;
					break;
				}
			}
		if(!$found || $layout===false)
			return false; // layout not found or file read error

		// save prepare object in case it's overwritten inside widget_Execute
		if(!is_null($prepare_object)) {
			$backup_prepare_object = &$this->_prepare_object;
			$this->_prepare_object = &$prepare_object;
		}
		$output = preg_replace_callback('/(?:<\?|~\?)([\w\d.]+):?([^:\?]*):?(.*?)(?:\?>|\?~)/s', array($this, 'widget_Execute'), $layout); // return the transformed layout

		// restore previous prepare object
		if(isset($backup_prepare_object))
			$this->_prepare_object = &$backup_prepare_object;
		return $output;
	}

	/**
	 * Returns an already loaded widget object or null; Widgets could use information from other widgets
	 *
	 * @param string $widget_selector Alias or selector of this widget
	 */
	public function &__get($widget_selector)
	{
		if(isset($this->_widgets[$widget_selector]))
			return $this->_widgets[$widget_selector];
		else {
			$null = null;
			return $null;
		}
	}

	/**
	 * Checks if a widget using an alias/name is loaded
	 * @param unknown_type $widget_selector
	 */
	public function __isset($widget_selector)
	{
		return isset($this->_widgets[$widget_selector]);
	}

	/**
	 * Forwards a call to the layout module to the current prepare object
	 *
	 * @param callback $prepare_method
	 * @param mixed $args
	 */
	public function __call($prepare_method, $args)
	{
		call_user_func_array(array($this->_prepare_object, $prepare_method), $args);
	}

	/**
	 * Removes the widget cache and reference to _prepare_object when asked to self-clean
	 */
	public function self_Clean()
	{
		$this->_widgets = array();
		$this->_prepare_object = null;
	}

}

namespace DRY\Layout\Widget;

/**
 * DonReY Framework 2012 :: Base class for Widgets
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/layout.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module\Layout
 */
abstract class base
{
	/**
	 * This method is called when encountering the widget identifier inside the layout: <?widget.identifier?>
	 * All widgets must implement render() or they won't be able to output
	 * @param mixed $prepare_return Incoming data from the "Prepare" method (or null if there was none)
	 * @param \DRY\Module\Layout $layout_module The layout module which called this widget (optional)
	 */
	abstract public function render($prepare_return = null, \DRY\Module\Layout $layout_module = null);
}